在UNIAPP中使用GraphQL

提醒:本文最后更新于 626 天前,文中所描述的信息可能已发生改变,请谨慎使用。

最近接到一个需求,需要在UNIAPP编写的微信小程序中使用GraphQL,一番折腾算是跑通了,写个帖子记录一下。

安装依赖

项目中的UNIAPP中使用的是Vue2,搜索了一下有个名为 Vue Apollo 的库可以使用,按照文档的教程,首先安装依赖包。

npm install --save vue-apollo graphql apollo-boost

然后创建Apollo ProviderApollo Client,然后安装插件到Vue

import Vue from 'vue';
import VueApollo from 'vue-apollo';
import ApolloClient from 'apollo-boost';

Vue.use(VueApollo);

const apolloClient = new ApolloClient({
  uri: 'https://test.tt/api/graphql'
});

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
});


const app = new Vue({
  ...App,
  apolloProvider
});

app.$mount();

Fetch

启动项目后控制台报错提示找不到fetch,这是因为小程序环境中没有fetchApolloClient提供了自定fetch的选项,文档见此: https://www.apollographql.com/docs/react/networking/advanced-http-networking/#custom-fetching

修改之前创建Apollo Client的代码:


const createResponse = (data) => {
    return {
        async text() {
            return JSON.stringify(data);
        }
    }
}

const apolloClient = new ApolloClient({
    uri: 'https://test.tt/api/graphql'
    fetch(url, { body, headers, method }) {

        const finalHeaders = {
            ...headers
        };

        return uni.request({
            url,
            data: body,
            header: finalHeaders,
            method,
            timeout: 10000,
        }).then(res => {
            return createResponse(res.data);
        }).catch(err => {
            return createResponse({
                errors: [
                    {
                        message: err.message || "网络错误"
                    }
                ]
            });
        });
    },
    onError(error) {
        // 全局错误提示
        const { response, networkError } = error;
        const Tips = (title) => uni.showToast({ title, icon: "none"});
        if (response && response.errors && response.errors.length) {
            Tips(response.errors[0].message);
        } else if (networkError && networkError.result && networkError.result.msg) {
            Tips(networkError.result.msg);
        } else if (networkError && networkError.message) {
            Tips(networkError.message);
        } else {
            Tips("网络错误");
        }
    },
});

ApolloClient会调用fetch返回的Response上面的text方法,而小程序中不存在Response,所以上面写了createResponse方法来模拟Response

配置loader

在实际开发时会将GraphQL查询文件按照模块写在.gql文件中,方便复用和管理。使用.gql文件需要配置webpack相关loader,以便能正确加载.gql文件。

Google 随便搜了一下就有配置,修改项目根目录的vue.config.js文件,加入以下配置:

module.exports = {
    chainWebpack: (config) => {
        config.module
            .rule('graphql')
            .test(/\.(graphql|gql)$/)
            .use("graphql-tag/loader")
            .loader("graphql-tag/loader")
            .end();
    }
}

启动项目后提示找不到graphql-tag/loader,但是之前装好的apollo-boost包已经依赖了此包,我猜测是UNIAPP发现package.json中没有显式依赖graphql-tag/loader,所以报了这个错。

可以安装graphql-tag/loader来避免这个错误,也可以手动引入graphql-tag/loader,像这样:

const path = require("path");

const gqlLoader = path.resolve(__dirname, "node_modules/graphql-tag/loader.js");

module.exports = {
    chainWebpack: (config) => {
        config.module
            .rule('graphql')
            .test(/\.(graphql|gql)$/)
            .use(gqlLoader)
            .loader(gqlLoader)
            .end();
    }
}

配置 apolloProvider

上面的loader配置好之后,理论上就可以正常使用GrahpQL了。像这样:

// product.gql
query productList($page: Int!, $limit: Int!) {
    productList(page: $page, limit: $limit) {
        list {
            id
            title
            cover
        }
        total
    }
}
import productQL from "./product.gql";

export default {
    apollo: {
        productList: {
            query: productQL.productList
            variables() {
                return {
                    ...this.form
                };
            }
        }
    },
    data() {
        return {
            productList: {
                list: [],
                total: 0
            },
            form: {
                page: 1,
                limit: 15
            }
        };
    }
}

理论上进入此页面后就会自动执行请求,然后现实很骨感,并没有发出任何请求。

经过查阅vue-apollo源码,每个用到vue-apollo的页面都会依赖this.$apolloProvider属性,这个属性是从自身的$options.apolloProvider或者父组件的$apolloProvider中获取来的。

UNIAPP中只有App.vue这个页面拥有$options.apolloProvider属性,而其他页面并不是App.vue的子组件;要想正常使用graphql,需要在用到graphql的页面手动注入apolloProvider属性。

我的做法是将apolloProvider属性挂载到全局的globalData上,然后手动注入属性,见如下代码:

App.vue中加入如下全局配置:

export default {
	globalData: {
		apolloProvider: null
	},
    onLaunch() {
		this.globalData.apolloProvider = this.$options.apolloProvider;
	}
}

在用到graphql的页面中加入如下配置:

export default {
    apolloProvider() {
		return getApp().globalData.apolloProvider;
	}
}

这样一来就可以正常使用graphql了。

PS:如果类似$apollo.queries.ping.loading这样的加载状态不生效,可以使用$apolloData.queries.ping.loading来代替。

配合ThinkPHP使用

本次开发后端使用了TP6,也简单记录一下吧。

首先安装graphql-php包,然后新建一个控制器,前端的路径指向这个控制器和对应的方法。


use app\utils\GraphUtil;
use GraphQL\GraphQL;

class GraphQLController extends Controller
{
    protected $isLogin = false;

    protected $userInfo = [];

    protected $uid = 0;

    protected $schema;

    protected function initialize()
    {
        $data = $this->request->postMore([
            ['operationName', ''],
            ['query', ''],
        ]);

        try {
            // 可以在这里进行鉴权

            // ...

            $this->isLogin = true;
            $this->uid = 114514;
            $this->userInfo = [
                'nickname' => '田所浩二'
            ];

        } catch (\Throwable $e) {
            // ...balalala
        }

        $typesConfig = config('graph.types.api');

        $this->schema = GraphUtil::getSchema($typesConfig);


        // 根据查询和变更名称检查是否有权限访问
        if (!$this->isLogin) {
            $noAuthQuery = GraphUtil::getNoAuthQuery($typesConfig);

            $noAuthQueryMap = array_reduce($noAuthQuery, function ($res, $query) {
                $res[$query] = 1;
                return $res;
            }, []);

            try {
                $currentQuery = GraphUtil::parseQuery($query);
                foreach ($currentQuery as $query) {
                    if (!isset($noAuthQueryMap[$query])) {
                        // ...balalala 没有权限,禁止访问~~~
                    }
                }
            } catch (\Throwable $e) {
            }
        }
    }

    public function index()
    {
        $data = $this->request->postMore([
            ['query', ''],
            ['variables', []]
        ]);
        // `variables`参数是前端传递上来的参数,`query`是前端传递上来的`graphql`查询。


        $rootData = [
            'uid' => $this->uid,
            'userInfo' => $this->userInfo,
            'isLogin' => $this->isLogin
        ];

        // `rootData`可以自定义,用来传递鉴权后的用户信息。

        $typesConfig = config('graph.types.api');

        $result = GraphQL::executeQuery($this->schema, $data['query'], $rootData, null, $data['variables']);

        $output = $result->toArray();

        return json()->data($output)->code(200);
    }
}
<?php
// config\graph.php

return [
    'types' => [
        'api' => [
            'query' => [
                app\api\graphql\common\CommonQuery::class
            ],
            'mutation' => [
                app\api\graphql\common\CommonMutation::class,
            ]
        ]
    ]
];

上面这个配置文件记录了所有的graphql查询和变更文件。

<?php

namespace app\utils;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Schema;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser;
use GraphQL\Language\Visitor;
use ReflectionClass;

class GraphUtil
{
    // 获取所有的Graphql相关文件并组装为Schema
    public static function getSchema($config)
    {
        $queryTypesConfig = $config['query'];
        $mutationTypesConfig = $config['mutation'];

        $queryTypes = [];
        $mutationTypes = [];

        foreach ($queryTypesConfig as $types) {
            $queryTypes = array_merge($queryTypes, $types::getTypes());
        }

        foreach ($mutationTypesConfig as $types) {
            $mutationTypes = array_merge($mutationTypes, $types::getTypes());
        }

        $schema = new Schema([
            'query' => new ObjectType([
                'name' => 'Query',
                'fields' => $queryTypes
            ]),
            'mutation' => new ObjectType([
                'name' => 'Mutation',
                'fields' => $mutationTypes
            ])
        ]);

        return $schema;
    }

    // 获取所有不需要鉴权的查询和变更
    public static function getNoAuthQuery($config)
    {
        $queryTypesConfig = $config['query'];
        $mutationTypesConfig = $config['mutation'];

        $noQueryResult = [];

        $propertyName = "NOT_AUTH";

        foreach ($queryTypesConfig as $types) {
            $class = new ReflectionClass($types);

            if ($class->hasConstant($propertyName) && is_array($class->getConstant($propertyName))) {
                $noQueryResult = array_merge($noQueryResult, $class->getConstant($propertyName));
            }
        }

        foreach ($mutationTypesConfig as $types) {
            $class = new ReflectionClass($types);

            if ($class->hasConstant($propertyName) && is_array($class->getConstant($propertyName))) {
                $noQueryResult = array_merge($noQueryResult, $class->getConstant($propertyName));
            }
        }
        return $noQueryResult;
    }

    // 从前端传递的查询/变更中获取查询/变更的名称
    public static function parseQuery($query)
    {
        $operations = [];
        Visitor::visit(
            Parser::parse($query),
            [
                NodeKind::OPERATION_DEFINITION => function ($node) use (&$operations) {
                    $selections = array_map(function ($selection) {
                        return $selection['name']['value'];
                    }, $node->toArray()['selectionSet']['selections']);

                    foreach ($selections as $selection) {
                        $operations[] = $selection;
                    }

                    return Visitor::stop();
                }
            ]
        );

        return $operations;
    }
}

以上代码是graphql相关工具类。

<?php
namespace app\api\graphql\common;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class CommonQuery
{

    const NOT_AUTH = [
        "getBalalalal",
    ];

    public static function getTypes()
    {
        return [
            "getBalalalal" => [
                "type" => new ObjectType([
                    'name' => 'getBalalalal',
                    'fields' => [
                        'list' => Type::listOf(Type::string())
                    ]
                ]),
                'args' => [
                    'page' => Type::int(),
                    'limit' => Type::int()
                ],
                'resolve' => function ($rootData, $data) {
                    return self::getBalalalal($rootData, $data);
                }
            ]
        ];
    }

    public static function getBalalalal($rootData, $data)
    {
        if ($rootData['isLogin']) {
            // ...balalalalalala
        }

        $list = [
            "hahahahahahahahahhaahha"
        ];

        return compact("list");
    }
}

上方代码是graphql执行查询的示例。

Powered By Hexo & Theme Veni